<template>
  <div class="view-wrapper">
    <div class="user-login">
      <template v-if="!socketId">
        <el-form :inline="true" :model="formData">
          <el-form-item label="房间号">
            <el-input v-model="formData.roomId" />
          </el-form-item>
          <el-form-item label="用户名">
            <el-input v-model="formData.username" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="login">进入房间</el-button>
          </el-form-item>
        </el-form>
      </template>
      <template v-else>
        <el-button v-if="isPublisher && !hasStartLive" @click="startLive">开始直播</el-button>
        <el-button v-if="isPublisher && hasStartLive" @click="stopLive">停止直播</el-button>
        <el-button @click="logout">退出</el-button>
      </template>
    </div>
    <div class="container">
      <ul class="user-list">
        <li
          v-for="user in userList"
          :key="user.socketId"
          class="user-item"
        >
          <span>
            {{ user.username }}
            {{ `${user.socketId === currentUser.socketId ? '（我）' : ''}` }}
          </span>
          <i
            v-if="user.isPublisher"
            class="el-icon-service service-icon"
          ></i>
        </li>
      </ul>
      <div class="camera-box">
        <video
          ref="liveVideo"
          class="live-video"
          autoplay
          muted
        ></video>
      </div>
    </div>
  </div>
</template>

<script>
import { io } from 'socket.io-client'
export default {
  name: 'LiveBroadcast',
  data () {
    return {
      isPublisher: Boolean(this.$route.query.publisher),
      formData: {
        username: '',
        roomId: ''
      },
      socketId: null,
      userList: [],
      hasStartLive: false
    }
  },
  beforeDestroy () {
    this.socket && this.socket.disconnect()
  },
  mounted () {
    this.currentUser = {}
    this.publisher = {}
    this.rtcPcMap = new Map()
    this.liveVideo = this.$refs.liveVideo
  },
  methods: {
    log (text, type = 'log') {
      const time = new Date()
      console[type](`[${time.toLocaleTimeString()}] ${text}`)
    },
    handleGetUserMediaError (e) {
      this.log(e, 'trace')
      switch (e.name) {
      case 'NotFoundError':
        this.$message({
          message: 'Unable to open your call because no camera and/or microphone were found.',
          type: 'error'
        })
        break
      case 'SecurityError':
      case 'PermissionDeniedError':
        break
      default:
        this.$message({
          message: `Error opening your camera and/or microphone: ${e.message}`,
          type: 'error'
        })
        break
      }
      this.stopLive()
    },
    login () {
      this.socket = io('http://localhost:3000', {
        transports: ['websocket'],
        query: {
          ...this.formData,
          isPublisher: this.isPublisher
        }
      })
      this.socket.on('connect', () => {
        this.$message({
          message: '登录成功',
          type: 'success'
        })
        this.socketId = this.socket.id
        this.username = ''
      })
      this.socket.on('connect_error', () => {
        setTimeout(() => {
          this.socket.connect()
        }, 1000)
      })
      this.socket.on('disconnect', () => {
        this.$message({
          message: '退出成功',
          type: 'success'
        })
        this.socketId = null
        this.userList = []
      })
      this.socket.on('message', data => {
        this.log('Message received: ')
        console.dir(data)
        const { type } = data
        switch (type) {
        case 'userList':
          this.handleUserList(data)
          break
        case 'join':
          this.handleUserJoin(data)
          break
        case 'leave':
          this.handelUserLeave(data)
          break
          // 信令消息：用于建立 WebRTC 连接
        case 'newIceCandidate': // A new ICE candidate has been received
          this.handleCandidate(data)
          break
        case 'videoOffer': // Invitation and offer to chat
          this.handleVideoOffer(data)
          break
        case 'videoAnswer': // Callee has answered our offer
          this.handleVideoAnswer(data)
          break
        case 'hangUp':
          this.handleHangUp(data)
          break
        default:
          this.log('Unknown message received')
        }
      })
    },
    handleUserList ({ user, roomUserList }) {
      this.currentUser = { ...user }
      this.userList = roomUserList.slice()
      if (!this.isPublisher) {
        const publisher = roomUserList.find(item => item.isPublisher)
        if (publisher) {
          this.publisher = publisher
          this.targetUser = { ...this.publisher }
          this.requestConnection()
        }
      }
    },
    handleUserJoin ({ user }) {
      const { username, socketId } = user
      if (socketId === this.socketId) return
      this.$message({
        message: `${username}进入房间`
      })
      this.userList.push(user)
    },
    logout () {
      this.socket.disconnect()
      this.stopLive()
    },
    handelUserLeave ({ user }) {
      const { username, socketId } = user
      this.$message({
        message: `${username}离开房间`
      })
      const index = this.userList.findIndex(item => item.socketId === socketId)
      this.userList.splice(index, 1)
    },
    sendToServer (eventName, data) {
      const dataJSON = JSON.stringify(data)
      this.log(`emit '${eventName}' data: ${dataJSON}`)
      this.socket.emit(eventName, { ...data, from: this.currentUser, target: this.targetUser })
    },
    async startLive () {
      try {
        this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
        this.liveVideo.srcObject = this.localStream
        this.hasStartLive = true
      } catch (err) {
        this.handleGetUserMediaError(err)
      }
    },
    requestConnection () {
      this.log('开始准备发起连接直播申请')
      const pcKey = this.targetUser.socketId
      let pc = this.rtcPcMap.get(pcKey)
      if (pc) {
        this.$message({
          message: '正在观看直播',
          type: 'warning'
        })
        return
      }
      this.log(`正在建立与发布者的连接: ${this.publisher.username}`)
      pc = this.createPeerConnection()
      pc.addTransceiver('audio', { direction: 'recvonly' })
      pc.addTransceiver('video', { direction: 'recvonly' })
      this.rtcPcMap.set(pcKey, pc)
      // try {
      //   // 只建立音视频轨道，并不需要使用摄像头、麦克风，使用addTransceiver()
      //   this.localStream.getTracks().forEach(this.transceiver = track => this.myPeerConnection.addTransceiver(track, { streams: [this.localStream] }))
      // } catch (err) {
      //   this.handleGetUserMediaError(err)
      // }
    },
    createPeerConnection () {
      const peerConnection = new RTCPeerConnection()
      peerConnection.onicecandidate = this.handleICECandidateEvent
      peerConnection.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent(peerConnection)
      peerConnection.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent(peerConnection)
      peerConnection.onsignalingstatechange = this.handleSignalingStateChangeEvent(peerConnection)
      peerConnection.onnegotiationneeded = !this.isPublisher ? () => this.handleNegotiationNeededEvent(peerConnection) : null
      peerConnection.ontrack = this.handleTrackEvent
      peerConnection.onremovetrack = this.handleRemoveTrackEvent
      return peerConnection
    },
    // 处理 icecandidate 事件
    // 通过信令服务器将本地 ICE 候选发送给另一个对等端
    handleICECandidateEvent (event) {
      if (event.candidate) {
        this.log(`*** Outgoing ICE candidate: ${event.candidate.candidate}`)
        this.sendToServer('newIceCandidate', { candidate: event.candidate })
      }
    },
    // 处理 iceconnectionstatechange 事件，检测ICE连接的状态：closed、failed、disconnected
    // 当ICE代理的状态发生变化时调用
    handleICEConnectionStateChangeEvent (peerConnection) {
      this.log(`*** ICE connection state changed to ${peerConnection.iceConnectionState}`)
      switch (peerConnection.iceConnectionState) {
      case 'closed':
      case 'failed':
      case 'disconnected':
        this.stopLive()
        break
      }
    },
    // 处理 icegatheringstatechange 事件
    // ICE 候选收集进程状态发生更改时调用
    // new： 没有建立连接，gathering：正在收集， complete：完成收集
    handleICEGatheringStateChangeEvent (peerConnection) {
      this.log(`*** ICE gathering state changed to: ${peerConnection.iceGatheringState}`)
    },
    // 处理 signalingstatechange 事件
    // 当ICE 信令状态更改时调用
    handleSignalingStateChangeEvent (peerConnection) {
      this.log(`*** WebRTC signaling state changed to: ${peerConnection.signalingState}`)
      switch (peerConnection.signalingState) {
      case 'closed':
        this.stopLive()
        break
      }
    },
    // 处理 negotiationneeded 事件
    // 一旦媒体数据连接到 RTCPeerConnection，就会在连接处触发事件，启动 ICE 协商
    async handleNegotiationNeededEvent (peerConnection) {
      this.log('*** Negotiation needed')
      try {
        this.log('---> 创建SDP请求')
        // 创建一个 SDP 请求并将其发送给我们想要连接的对等端
        const offer = await peerConnection.createOffer()

        // 如果连接尚未达到“stable”状态，则返回到调用方。当状态稳定时，将触发另一个需要协商的事件
        if (peerConnection.signalingState !== 'stable') {
          this.log('     -- 连接尚未达到“stable”状态; postponing...')
          return
        }

        // 当 createOffer() 成功（执行 promise）时
        // 创建的请求信息传递到myPeerConnection.setLocalDescription()，它为调用方的连接端配置连接和媒体配置状态
        this.log('---> 将创建的请求信息设置本地描述信息')
        await peerConnection.setLocalDescription(offer)

        // 创建了一个包含本地描述的新 "videoOffer" 消息，通过信令服务器将请求发送给被叫方
        this.log('---> 发送请求给对方')
        this.sendToServer('videoOffer', { sdp: peerConnection.localDescription })
      } catch (err) {
        this.log('*** 处理 negotiationneeded 事件时发生以下错误:')
        this.log(`Error ${err.name}: ${err.message}`, 'trace')
      }
    },
    handleTrackEvent (event) {
      this.log('*** Track event')
      this.liveVideo.srcObject = event.streams[0]
    },
    handleRemoveTrackEvent (event) {
      this.log('*** Remove track event')
      const stream = this.liveVideo.srcObject
      const trackList = stream.getTracks()
      if (trackList.length === 0) {
        this.stopLive()
      }
    },
    async handleCandidate ({ from, candidate }) {
      const icecandidate = new RTCIceCandidate(candidate)
      this.log(`*** Adding received ICE candidate: ${JSON.stringify(icecandidate)}`)
      try {
        const pcKey = from.socketId
        const pc = this.rtcPcMap.get(pcKey)
        await pc.addIceCandidate(icecandidate)
      } catch (err) {
        this.log(`Error ${err.name}: ${err.message}`, 'trace')
      }
    },
    async handleVideoOffer ({ from, sdp }) {
      this.targetUser = { ...from }
      this.log('收到连接申请')
      const pcKey = from.socketId
      const pc = this.createPeerConnection()
      this.rtcPcMap.set(pcKey, pc)
      const desc = new RTCSessionDescription(sdp)
      if (pc.signalingState !== 'stable') {
        this.log('  - 信令状态不稳定, 触发rollback')

        // ?? Set the local and remove descriptions for rollback; don't proceed until both return.
        await Promise.all([
          pc.setLocalDescription({ type: 'rollback' }),
          pc.setRemoteDescription(desc)
        ])
        return
      } else {
        this.log('  - 设置远程描述信息')
        await pc.setRemoteDescription(desc)
      }
      if (this.localStream) {
        try {
          this.localStream.getTracks().forEach(track => {
            pc.addTrack(track, this.localStream)
          })
        } catch (err) {
          this.handleGetUserMediaError(err)
        }
      }
      this.log('---> 创建并发送应答给呼叫者')
      await pc.setLocalDescription(await pc.createAnswer())
      this.sendToServer('videoAnswer', { sdp: pc.localDescription })
    },
    async handleVideoAnswer ({ sdp }) {
      this.log('*** 对方已接听')
      const pcKey = this.targetUser.socketId
      const pc = this.rtcPcMap.get(pcKey)
      const desc = new RTCSessionDescription(sdp)
      await pc.setRemoteDescription(desc).catch((err) => {
        this.log(`Error ${err.name}: ${err.message}`, 'trace')
      })
    },
    stopLive () {
      this.log('退出直播')
      if (this.targetUser) {
        const pcKey = this.targetUser.socketId
        const peerConnection = this.rtcPcMap.get(pcKey)
        if (peerConnection) {
          this.log('--> 关闭 peer connection')

          // 移除所有事件监听
          peerConnection.onicecandidate = null
          peerConnection.oniceconnectionstatechange = null
          peerConnection.onicegatheringstatechange = null
          peerConnection.onsignalingstatechange = null
          peerConnection.onnegotiationneeded = null
          peerConnection.ontrack = null
          peerConnection.onremovetrack = null

          // 关闭 peer connection
          peerConnection.close()
          this.rtcPcMap.delete(pcKey)
          this.targetUser = null
        }
      }
      // 停止视频播放和track
      if (this.liveVideo.srcObject) {
        this.liveVideo.pause()
        this.liveVideo.srcObject.getTracks().forEach(track => {
          track.stop()
        })
        this.liveVideo.srcObject = null
        if (this.isPublisher) {
          this.hasStartLive = false
        }
      }
      this.localStream = null
    }
  }
}
</script>

<style lang="scss" scoped>
.view-wrapper {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

.container {
  display: flex;
  height: 100%;

  .user-list {
    overflow: auto;
    width: 240px;
    height: 100%;
    margin-right: 10px;
    border-right: 1px solid #ccc;

    .user-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 4px 8px;

      .service-icon {
        font-size: 16px;
      }
    }
  }

  .camera-box {
    display: block;
    flex: 1;
    overflow: auto;
    height: 100%;

    .live-video {
      object-fit: contain;
      width: 100%;
      height: 100%;
      border: 1px solid rgba(255, 255, 255, 0.75);
    }
  }
}
</style>
